package nl.utwente.ewi.hmi.multitouch;

import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import javolution.util.FastMap;

import nl.utwente.ewi.hmi.multitouch.analysis.Hypothesis;
import nl.utwente.ewi.hmi.multitouch.analysis.HypothesisCreator;
import nl.utwente.ewi.hmi.multitouch.analysis.HypothesisFilter;
import nl.utwente.ewi.hmi.multitouch.analysis.Segment;
import nl.utwente.ewi.hmi.multitouch.analysis.SideEffectFilter;
import nl.utwente.ewi.hmi.multitouch.analysis.segmentation.FrameSegmentation;
import nl.utwente.ewi.hmi.multitouch.io.TouchDeviceException;
import nl.utwente.ewi.hmi.multitouch.io.TouchStream;
import nl.utwente.ewi.hmi.multitouch.io.TouchStream.Frame;
import nl.utwente.ewi.hmi.multitouch.io.TouchStream.FrameBuffer;
import nl.utwente.ewi.hmi.multitouch.util.Properties;

/**
 * The TouchDevice acts as the layer between raw, but preprocessed, multi-user / multi-tangibility / multi-touch input and 
 * the abstract concept of touches. By reading {@linkplain Frame frames} from a {@linkplain TouchStream} and analyzing these,
 * {@linkplain Touch touches} will be generated and can be tracked. For ambiguous readings, the TouchDevice applies 
 * 
 * @author Michiel Hakvoort
 * @version 1.0
 */
public class TouchDevice {

	/**
	 * The {@linkplain TouchStream} from which {@linkplain TouchStream.Frame Frames} are being read
	 */
	protected final TouchStream touchStream;

	/**
	 * The {@linkplain Set} of registered {@linkplain TouchDeviceListener TouchDeviceListeners}
	 */
	protected final Set<TouchDeviceListener> touchDeviceListeners;

//	private TouchHistory touchHistory = null;
	
	/**
	 * The thread responsible for retrieving frames. 
	 */
	private final TouchStreamReader reader;

	private final Map<Segment, SegmentTouch> touches;

	/**
	 * Frame segmentation.
	 */
	private FrameSegmentation segmentation;

	/**
	 * Segment analysis.
	 */
	private final HypothesisCreator hypothesisCreator;

	/**
	 * Hypothesis filtering.
	 */
	private final HypothesisFilter hypothesisFilter;

	
	private final TouchFrameProcessor processor;
	
	private boolean isThreaded = true;
	
	/**
	 * Create a new TouchDevice for the specified {@linkplain TouchStream}.
	 * 
	 * @see TouchStream
	 * @param touchStream The {@linkplain TouchStream} to create the TouchDevice for
	 */
	public TouchDevice(TouchStream touchStream) {
		this.touchStream = touchStream;
		this.touchDeviceListeners = new CopyOnWriteArraySet<TouchDeviceListener>();

		this.processor = new TouchFrameProcessor();
		this.reader = new TouchStreamReader(processor);

//		this.segmentation = Properties.SEGMENTATION_FACTORY.getFrameSegmentation(this.touchStream.getTouchDeviceInfo());
		this.hypothesisCreator = Properties.HYPOTHESIS_FACTORY.getHypothesisCreator(this.touchStream.getTouchDeviceInfo()); 
		this.hypothesisFilter = new SideEffectFilter();

		this.touches = new HashMap<Segment, SegmentTouch>();

	}

	/**
	 * Set the TouchDevice to be either threaded or non threaded.
	 *  
	 * @param isThreaded
	 */
	public void setThreaded(boolean isThreaded) {
		this.isThreaded = isThreaded;
	}

	public boolean isThreaded() {
		return this.isThreaded;
	}

	/**
	 * Start the TouchDevice
	 * 
	 * @throws TouchDeviceException
	 */
	public synchronized void start() throws TouchDeviceException {
		// Only start the TouchDevice when the stream is not open
		if (!this.touchStream.isOpen()) {
			try {
				// Only start the reader and notify the listeners when the touchstream can open
				if (this.touchStream.open()) {
					reader.start();

					fireTouchDeviceStarted();
				}
			// Handle exceptions with a TouchDeviceException
			} catch (IOException e) {
				throw new TouchDeviceException(e.getMessage());
			}
		}
	}

	/**
	 * Stop the TouchDevice.
	 * 
	 * @throws TouchDeviceException
	 */
	public synchronized void stop() throws TouchDeviceException {
		// Only close an open touchstream
		if (this.touchStream.isOpen()) {
			try {
				if (this.touchStream.close()) {
					fireTouchDeviceStopped();
				}
			} catch (IOException e) {
				throw new TouchDeviceException(e.getMessage());
			}
		}

		if (reader.isReading()) {
			reader.stop();
		}
	}

	/**
	 * Get information about the TouchDevice.
	 * 
	 * @return The information about the TouchDevice
	 */
	public TouchDeviceInfo getTouchDeviceInfo() {
		return this.touchStream.getTouchDeviceInfo();
	}

	/**
	 * Register a TouchDeviceListener. A registered TouchDeviceListener will be
	 * notified of any events generated by the TouchDevice. This method is
	 * thread safe.
	 * 
	 * @param listener The TouchDeviceListener to add;
	 */
	public void addTouchDeviceListener(TouchDeviceListener listener) {
		this.touchDeviceListeners.add(listener);
	}

	/**
	 * Unregister a TouchDeviceListener. The TouchDeviceListener will no longer
	 * get any updates. This method is thread safe.
	 * 
	 * @param listener The TouchDeviceListener to remove.
	 */
	public void removeTouchDeviceListener(TouchDeviceListener listener) {
		this.touchDeviceListeners.remove(listener);
	}

	/**
	 * Notify the listeners with a newly registered Touch instance
	 */
	protected void fireTouchRegistered(Touch touch) {
		for (TouchDeviceListener listener : this.touchDeviceListeners) {
			listener.touchRegistered(this, touch);
		}
	}

	/**
	 * Notify the listeners with the fact that the TouchDevice has been stopped
	 */
	protected void fireTouchDeviceStopped() {
		for (TouchDeviceListener listener : this.touchDeviceListeners) {
			listener.touchDeviceStopped(this);
		}
	}

	/**
	 * Notify the listeners with the fact that the TouchDevice has been started
	 */
	protected void fireTouchDeviceStarted() {
		for (TouchDeviceListener listener : this.touchDeviceListeners) {
			listener.touchDeviceStarted(this);
		}
	}

	/**
	 * Get the set of currently registered Touches. This method is thread safe.
	 * 
	 * @return The set of currently registered Touches.
	 */
	public Set<? extends Touch> getTouches() {
		return Collections.unmodifiableSet(new HashSet<Touch>(this.touches.values()));
	}

	/**
	 * Get the TouchFrameProcessor.
	 * 
	 * @return The TouchFrameProcessor.
	 */
	public TouchFrameProcessor getTouchFrameProcessor() {
		return this.processor;
	}
	
	/**
	 * The TouchFrameProcessor class is responsible for doing reads to the touch table and converting
	 * these into events. For single threaded applications the TouchFrameProcessor can be used to trigger a
	 * single cycle of reading and propagating events. 
	 *  
	 * @author Michiel Hakvoort
	 * @see TouchDevice.getTouchFrameProcessor()
	 */
	public final class TouchFrameProcessor {

		private final ReentrantLock listLock = new ReentrantLock();

		private final Condition listEmpty = listLock.newCondition();

		final List<Callable<Void>> releasedEvents;
		final List<Callable<Void>> events;
		
		private TouchFrameProcessor() {
			this.releasedEvents = new LinkedList<Callable<Void>>();
			this.events = new LinkedList<Callable<Void>>();
		}

		public void process() {
			if(TouchDevice.this.isThreaded()) {
				// Wait for the event lists to be readable
				listLock.lock();
			} else {
				// See whether the event lists are readable, if not return
				if(!listLock.tryLock()) {
					return;
				}
			}

			try {
				// Execute the events
				for(Callable<Void> event : events) {
						try {
							event.call();
						} catch (Exception e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
	
				}
	
				// Execute the released events
				for(Callable<Void> event : releasedEvents) {
						try {
							event.call();
						} catch (Exception e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
				}

				events.clear();
				releasedEvents.clear();
				
				listEmpty.signal();
			} finally {
				listLock.unlock();
			}
		}

	}
	
	/**
	 * The TouchStreamReader reads Frames from the TouchStream whenever the
	 * TouchDevice is started.
	 * 
	 * 
	 * @author Michiel Hakvoort
	 */
	private class TouchStreamReader implements Runnable {

		boolean isReading = false;

		Thread runner = null;

		private TouchFrameProcessor handler = null;
		
		TouchStreamReader(TouchFrameProcessor handler) {
			this.handler = handler;

			runner = new Thread(this, "TouchStreamReader:" + TouchDevice.this.touchStream.getTouchDeviceInfo().getDeviceID());
			runner.setDaemon(true);
			runner.start();
		}

		synchronized boolean isReading() {
			return this.isReading;
		}

		synchronized boolean isRunning() {
			return this.runner != null;
		}

		public void run() {
			while (isRunning()) {
				boolean abort = false;

				// Loop while not reading and not aborting
				synchronized (this) {
					while (!(isReading() || abort)) {
						// Take lock
						try {
							// Release lock
							this.wait();
						} catch (InterruptedException ie) {
							// Abort for unexpected behaviour
							abort = true;
						}

						// Abort cycle when not running or interrupted
						abort = abort || !isRunning();
					}
				}

				// Continue to outer loop straight away
				if (abort) {
					continue;
				}

				// Write to the buffer, possibly not "get" the segments, but read into (buffer wise)
				final Set<Segment> segments = new HashSet<Segment>();

				try {
					touchStream.read(segments);
				} catch (IOException e) {
					e.printStackTrace();
					try {
						TouchDevice.this.stop();
					} catch (TouchDeviceException e1) {
						e1.printStackTrace();
					}
					continue;
				}

				// If something is written to the buffer, acquire the write lock
					// Read all segments from the buffer
					
					// reverse control :
					// final Set<Segment> segments = "frameBuffer".getSegments(TouchDevice.this.segmentation);
					
//					final Set<Segment> segments = TouchDevice.this.segmentation.segment(frameBuffer);
					
					// Update the hypothesis creator with the new segments, and get new hypothesis.
					Set<Hypothesis> hypotheses = TouchDevice.this.hypothesisCreator.getHypotheses(segments);

					// Filter hypotheses for side effects
					if(TouchDevice.this.touchStream.getTouchDeviceInfo().isAmbiguous()) {
						hypotheses = hypothesisFilter.filter(hypotheses);
					}
					
					final Map<Segment, Set<Segment>> fromHypotheses = new HashMap<Segment, Set<Segment>>(); //FastMap.newInstance();
					final Map<Segment, Set<Segment>> toHypotheses = new HashMap<Segment, Set<Segment>>(); //FastMap.newInstance();

					
					// Acquire a lock on the event list
					handler.listLock.lock();

					try {
						
						// Wait for the event list to be empty, releasing the lock for as long as it isn't
						while(!handler.events.isEmpty() || !handler.releasedEvents.isEmpty()) {
							handler.listEmpty.awaitUninterruptibly();
						}
						
						/*
						 * First, handle the simple hypotheses
						 * And create a todo list for the more complex hypotheses
						 */
						for(Hypothesis hypothesis : hypotheses) {
							Segment fromSegment = hypothesis.getFromSegment();
							Segment toSegment = hypothesis.getToSegment();
	
							// The Hypothesis marks the beginning of a trajectory
							// Fire a touch registered event
							if(hypothesis.isStartOfTrajectory()) {
								final SegmentTouch newTouch = new SegmentTouch(toSegment);
								TouchDevice.this.touches.put(hypothesis.getToSegment(), newTouch);
								handler.events.add(new Callable<Void>() {
									@Override
									public Void call() throws Exception {
										fireTouchRegistered(newTouch);
										return null;
									}
								});
								continue;
							}
	
							// The Hypothesis marks the ending of a trajectory
							// Fire a touch released event
							if(hypothesis.isEndOfTrajectory()) {
								final SegmentTouch removedTouch = TouchDevice.this.touches.get(fromSegment);
								handler.releasedEvents.add(new Callable<Void>() {
									@Override
									public Void call() throws Exception {
										removedTouch.fireTouchReleased();
										return null;
									}
								});
	
								TouchDevice.this.touches.remove(hypothesis.getFromSegment());
	
								continue;
							}
	
							// Build reverse relationships for investing joins and splits
							if(!fromHypotheses.containsKey(hypothesis.getFromSegment())) {
								fromHypotheses.put(hypothesis.getFromSegment(), new HashSet<Segment>());
							}
							fromHypotheses.get(hypothesis.getFromSegment()).add(hypothesis.getToSegment());
	
							if(!toHypotheses.containsKey(hypothesis.getToSegment())) {
								toHypotheses.put(hypothesis.getToSegment(), new HashSet<Segment>());
							}
							toHypotheses.get(hypothesis.getToSegment()).add(hypothesis.getFromSegment());				
						}
	
						/*
						 * Create join events, and cancel any normal tracking hypotheses if they are
						 * part of a join event.
						 */
						for(Entry<Segment, Set<Segment>> entry : toHypotheses.entrySet()) {
							Segment toSegment = entry.getKey();
							Set<Segment> fromSegments = entry.getValue();
	
							// A touch is normally tracked, this will get handled in the fromHypotheses set
							if(fromSegments.size() == 1) {
								continue;
							}
	
							// A touch is joined
							
							final SegmentTouch touch = new SegmentTouch(toSegment);
	
							TouchDevice.this.touches.put(toSegment, touch);
	
							handler.events.add(new Callable<Void>() {
								@Override
								public Void call() throws Exception {
									TouchDevice.this.fireTouchRegistered(touch);
									return null;
								}
							});
	
							final Set<Touch> joinedTouches = new HashSet<Touch>();
	
							/*
							 * Collect the segments from which the segment is joined
							 */
							for (Segment fromSegment : fromSegments) {
								final SegmentTouch joinedTouch = TouchDevice.this.touches.get(fromSegment);
								joinedTouches.add(joinedTouch);
	
								Set<Segment> toSegments = fromHypotheses.get(fromSegment);
	
								// If the from segment does not split, don't issue a single track update
								if(toSegments.size() < 2) {
									fromHypotheses.remove(fromSegment);
									TouchDevice.this.touches.remove(fromSegment);
	
									handler.releasedEvents.add(new Callable<Void>() {
										@Override
										public Void call() throws Exception {
											joinedTouch.fireTouchReleased();
											return null;
										}
									});
								}
	
								// Issue a touch join event for the joined touch
								handler.events.add(new Callable<Void>() {
									@Override
									public Void call() throws Exception {
										joinedTouch.fireTouchJoined(joinedTouches, touch);
										return null;
									}
								});
							}
						}
						
						/*
						 * Investigate normal tracks and splits
						 */
						for(Entry<Segment, Set<Segment>> entry : fromHypotheses.entrySet()) {
							Segment fromSegment = entry.getKey();
							Set<Segment> toSegments = entry.getValue();
	
							// The touch was not joined (it is still a valid hypothesis) and thus tracked
							if(toSegments.size() == 1) {
								 final SegmentTouch touch = TouchDevice.this.touches.remove(fromSegment);
								 final Segment toSegment = toSegments.iterator().next();
	
								 TouchDevice.this.touches.put(toSegment, touch);
								 
								 handler.events.add(new Callable<Void>() {
									 @Override
									public Void call() throws Exception {
										if(touch == null) { return null; }
										touch.updateSegment(toSegment);
										return null;
									}
								 });
								 continue;
							}
	
							// A segment is split
							final SegmentTouch touch = TouchDevice.this.touches.get(fromSegment);
							final Set<Touch> toTouches = new HashSet<Touch>();
							TouchDevice.this.touches.remove(fromSegment);
	
							handler.releasedEvents.add(new Callable<Void>() {
								@Override
								public Void call() throws Exception {
									touch.fireTouchSplitted(touch, toTouches);
									touch.fireTouchReleased();
									return null;
								}
							});
	
							/*
							 *  Fill toTouches with the touches that it is split into
							 */
							for(Segment toSegment : toSegments) {
								final SegmentTouch toTouch;
								
								// If a "toSegment" was already part of a join, reuse that segment
								if(!TouchDevice.this.touches.containsKey(toSegment)) {
									toTouch = new SegmentTouch(toSegment);
	
									TouchDevice.this.touches.put(toSegment, toTouch);
									handler.events.add(new Callable<Void>() {
										@Override
										public Void call() throws Exception {
											TouchDevice.this.fireTouchRegistered(toTouch);
											return null;
										}
									});
								} else {
									toTouch = TouchDevice.this.touches.get(toSegment);
								}
								
								toTouches.add(toTouch);
							}
	
						}
					} finally {
						// Release the list lock
						handler.listLock.unlock();
					}

					if(TouchDevice.this.isThreaded()) {
						handler.process();
					}

				}			
		}

		synchronized void stop() {
			this.isReading = false;

			// Not needed, but allow a clean exit
			this.notifyAll();
		}

		synchronized void start() {
			this.isReading = true;
			this.notifyAll();
		}
	}

	/**
	 * Return the set of actors which are capable of interacting with the TouchDevice.
	 * 
	 * @return The set of actors capable of interacting with the TouchDevice
	 */
	public Set<Object> getActors() {
		return this.touchStream.getActors();
	}
	
	private class SegmentTouch extends Touch {

		Segment segment = null;

		AffineTransform transform = null;
		
		Point2D transformedPoint = new Point2D.Float();

		SegmentTouch(Segment segment) {
			super(new Date(), segment.getTangible());
			
			this.touchDevice = TouchDevice.this;

			this.transform = TouchDevice.this.touchStream.getTouchDeviceInfo().getTransform();

			
			
			this.isAmbiguous = segment.isAmbiguous();
			this.bounds = segment.getBounds();
			this.origin = new Point2D.Float();
			this.originStart = new Point2D.Float();

			transform.transform(segment.getOrigin(), this.origin);
			transform.transform(segment.getOrigin(), this.originStart);

			this.relativeOrigin = new Point2D.Float((float)(segment.getOrigin().getX() / TouchDevice.this.getTouchDeviceInfo().getSize().getWidth())
					,(float)(segment.getOrigin().getY() / TouchDevice.this.getTouchDeviceInfo().getSize().getHeight()));
			
			this.relativeOriginStart = new Point2D.Float((float)relativeOrigin.getX(), (float)relativeOrigin.getY());
			
			
			
			this.shape = segment.getPerimeter();
			this.path = new GeneralPath();
			this.path.moveTo(this.origin.getX(), this.origin.getY());

			this.segment = segment;
			this.velocity = new Point2D.Float(0, 0);
			
		}

		void updateSegment(Segment segment) {
			float dTime = (segment.getTime() - this.segment.getTime()) / 1000f;

			this.bounds = segment.getBounds();

			transform.transform(segment.getOrigin(), transformedPoint);

			this.relativeOrigin.setLocation((float)(segment.getOrigin().getX() / TouchDevice.this.getTouchDeviceInfo().getSize().getWidth())
					,(float)(segment.getOrigin().getY() / TouchDevice.this.getTouchDeviceInfo().getSize().getHeight()));
			this.velocity = new Point2D.Float((float) (transformedPoint.getX() - this.origin.getX()) / dTime, (float) (transformedPoint.getY() - this.origin.getY()) / dTime);
			
			if(this.origin.distance(transformedPoint) > 0) {
				transform.transform(segment.getOrigin(), this.origin);
				this.path.lineTo(this.origin.getX(), this.origin.getY());

				this.fireTouchMoved();
			}

			// TODO : fix bounds update (combine with transform)
			if((Math.abs(this.bounds.getHeight() - segment.getBounds().getHeight()) > 2) || Math.abs(this.bounds.getWidth() - segment.getBounds().getWidth()) > 2) {
				this.bounds = segment.getBounds();
				this.fireTouchShapeChanged();
			}
			
			this.segment = segment;
		}
	}

}
